/*
 * MIT License
 *
 * Copyright (c) 2025 milkpotatoes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

export { };

const innerParse: (url: string, base?: string) => string[] = exchangeInnerMethod(globalThis, "_innerParse");
const applySearchString: (that: URLSearchParams, search: string) => void = Reflect.get(globalThis, 'applySearchString');

const splitOnce = (url: string, div: string) => {
    const index = url.indexOf(div);
    if (index === -1) {
        return [url, ''];
    }
    return [url.substring(0, index), url.substring(index + 1)];
};

function parseUrl(that: URL, url: string, base?: string) {
    const basePath = base ? splitOnce(String(base), '#')[0] : undefined;
    const [urlPath, urlHash] = splitOnce(String(url), '#');
    const result = arguments.length > 2 ? innerParse(urlPath, basePath) : innerParse(urlPath);
    [that.username, that.password] = splitOnce(result[1], ':');
    that.hostname = result[2];
    that.port = result[3];
    that.pathname = result[4];
    that.hash = urlHash !== '' ? `#${urlHash}` : '';
    let urlData = g_urlData.get(that);
    if (urlData !== undefined) {
        urlData.protocol = result[0] + ':';
        applySearchString(urlData.searchParams, result[5]);
    } else {
        g_urlData.set(that, {
            protocol: result[0] + ':',
            searchParams: new URLSearchParams(result[5]),
        });
    }
}

interface UrlData {
    protocol: string;
    searchParams: URLSearchParams;
}

const g_urlData = new WeakMap<URL, UrlData>();
const VALID_PROTOCOL = new Set(['http:', 'https:', 'ftp:', 'ws:', 'wss:', 'file:']);
const BLANK_REGEX = / /g;
const URI_ENCODED_BLANK = '%20';

class URL {
    static canParse(url: string, base?: string): boolean {
        try {
            new URL(url, base);
        } catch {
            return false;
        }
        return true;
    }

    static parse(url: string, base?: string): URL | null {
        try {
            return new URL(url, base);
        } catch {
            return null;
        }
    }

    get protocol(): string {
        return g_urlData.get(this)!.protocol;
    }
    set protocol(v: string) {
        if (!v.endsWith(':')) v += ':';
        if (VALID_PROTOCOL.has(v)) g_urlData.get(this)!.protocol = v;
    }
    username!: string;
    password!: string;
    get host(): string {
        return this.port !== '' ? `${this.hostname}:${this.port}` : this.hostname;
    };
    set host(v: string) {
        [this.hostname, this.port] = splitOnce(v, ':');;
    }
    hostname!: string;
    hash!: string;
    get href(): string {
        return this.toString();
    };
    set href(v: string) {
        parseUrl(this, v);
    }
    get origin(): string {
        return `${this.protocol}//${this.host}`;
    };
    pathname!: string;
    port!: string;
    get search(): string {
        const query = this.searchParams.toString();
        return query !== '' ? '?' + query : '';
    };
    set search(v: string) {
        applySearchString(this.searchParams, v);
    }
    get searchParams(): URLSearchParams {
        return g_urlData.get(this)!.searchParams;
    }
    constructor(url: string, base?: string) {
        if (arguments.length > 1) parseUrl(this, url.replace(BLANK_REGEX, URI_ENCODED_BLANK), base!.replace(BLANK_REGEX, URI_ENCODED_BLANK));
        else parseUrl(this, url.replace(BLANK_REGEX, URI_ENCODED_BLANK));
        if (!VALID_PROTOCOL.has(this.protocol))
            throw new TypeError("Failed to construct 'URL': Invalid URL");
    }
    toJSON(): string {
        return this.toString();
    }
    toString(): string {
        const userInfo = this.username + (this.password !== '' ? `:${this.password}` : '');
        return `${this.protocol}//${userInfo !== '' ? `${userInfo}@` : ''}${this.host}${this.pathname}${this.search}${this.hash}`;
    }
}

exportToObject(globalThis, "URL", URL, PropertyConfiguration.W_C);
